跳到主要内容

【教程】从零开始构建企业级高可用 PostgreSQL 集群

342.webp

作者序

本文转载自 我的博客站 ,原文地址:【教程】从零开始构建企业级高可用 PostgreSQL 集群

在一切开始之前,请注意以下事项:

  1. 本文默认你已经完成【教程】从零开始搭建最小高可用 k3s 集群
  2. 本文所使用的开发环境为 ArchLinux + Fish Shell,由于跨系统指令语法差异,请根据自己的实际情况调整指令格式。
  3. 本文所使用的对象存储服务为一款兼容 S3 协议的对象存储,请根据自己的实际情况调整配置方式。
  4. 本文中的指令依赖于各类CLI工具(已在文中标注或是在前置教程中标注,linux系统常见的CLI工具不会在文中标注),由于CLI工具的安装过程较为繁琐且对于各个平台的安装步骤并不一致,此处不再赘述,请参考官方文档的安装教程。

引言

PostgreSQL 是功能最强大的开源关系型数据库(具体优点网上已经被介绍过很多次了,这里不再赘述),这也是笔者用的最多的关系型数据库(没有之一),因此在本人的教程中,所使用的关系型数据库一般以PostgreSQL为主,因此你可以将这篇教程视作Kubernetes相关教程中的一篇非常基础的前置教程
接下来说下这个CloudNativePG(CNPG),它是什么东西呢?简而言之,你可以将它视作是PostgreSQLKubernetes的特化版本,大幅简化了PostgreSQL部署与管理的过程,同时也为了满足企业级的要求,它提供了很多的高级特性,包括高可用(HA)、自动化备份、故障演练、自动化扩展等。 本教程将基于 k3s + Longhorn,使用 CloudNativePG 以及通用的 S3 兼容对象存储,从零搭建一套高可用 PostgreSQL 集群,并演示基础的备份、运维操作,最后的最后,我们会手动创造故障,测试数据库的自动化恢复能力

前置条件

在开始部署前,请确保以下前提已经满足:

基础设施

首先,你需要准备一个类似于 【教程】从零开始搭建最小高可用 k3s 集群 中的最小高可用k3s集群

CLI 工具

在开始教程前,你需要准备以下 CLI 工具:

  1. kubectl,用于集群的管理,在上篇教程中,我们已经安装了 kubectl,并配置了对远程集群的管理权限(kubeconfig)
    • 验证指令:
      kubectl version --short
    • 理想输出:显示 Client Version: <client_version>;正常连通时会额外返回 Server Version: <server_version>
  2. helm, 用于部署 Operator,在上篇教程中,我们已经安装了 helm,并配置了对远程集群的管理权限(即kubeconfig)
    • 验证指令:
      helm version
    • 理想输出:version.BuildInfo 字段中包含 Version: <helm_version>GitCommit: <commit_hash> 等信息
  3. kubeseal, 用于加密 Kubernetes Secret,请参考官方教程自行安装
    • 验证指令:
      kubeseal --version
    • 理想输出:输出 kubeseal version: <kubeseal_version>
  4. openssl, 用于生成随机密码,这个一般系统自带,如果没有,可以参考官方教程自行安装
    • 验证指令:
      openssl version
    • 理想输出:输出 OpenSSL 版本信息(如 OpenSSL <openssl_version> ...

第三方基础设施

你需要准备一个 S3 兼容的对象存储服务账号(如 AWS S3、Wasabi、Backblaze B2 或自建 MinIO),并确保能够创建 Bucket 与访问密钥

部署架构概览

在本教程中,我们将实现如下操作:

  1. cnpg-system 命名空间中运行 CloudNativePG Operator。
  2. data-infra 命名空间中创建一个 3 副本的 PostgreSQL 集群(1 Primary + 2 Replica)。
  3. 启用 barman 持续备份到对象存储,并为应用暴露一个 ReadWrite Service。
  4. 借助cloudflared侧载容器安全地将数据库暴露到公网,用于开发测试。
  5. 运维演习,测试数据库的滚动更新与插件安装。
  6. 灾难演习,随机让节点爆炸,观察自动恢复能力。测试在极端环境下用对象存储中的备份数据重建数据库。
  7. 实现可观察性,通过PrometheusGrafana监控数据库状态。

349.webp

步骤一:安装 CloudNativePG Operator

官方推荐使用 Helm 部署 Operator。首先添加 Helm 仓库并完成安装:

helm repo add cloudnative-pg https://cloudnative-pg.github.io/charts
helm repo update

kubectl create namespace cnpg-system

helm upgrade --install cloudnative-pg cloudnative-pg/cloudnative-pg \
--namespace cnpg-system \
--set controllerManager.resources.requests.cpu=200m \
--set controllerManager.resources.requests.memory=256Mi \
--set controllerManager.resources.limits.cpu=500m \
--set controllerManager.resources.limits.memory=512Mi

结果如图:

353.webp 354.webp

安装指令参数解释:

参数/片段作用备注
helm upgrade --install若 Release 已存在则升级,否则首次安装保持部署命令幂等
cloudnative-pgHelm Release 的名称Helm 用于追踪本次部署
cloudnative-pg/cloudnative-pgChart 的仓库与名称需提前 helm repo add cloudnative-pg …
--namespace cnpg-system使用的 Kubernetes 命名空间Operator 将在该命名空间运行
--set controllerManager.resources.requests.cpu=200mController Manager 请求的最低 CPU保证调度至少 0.2 核
--set controllerManager.resources.requests.memory=256MiController Manager 请求的最低内存保证调度至少 256Mi
--set controllerManager.resources.limits.cpu=500mController Manager 的 CPU 上限超过 0.5 核将被限制
--set controllerManager.resources.limits.memory=512MiController Manager 的内存上限超过 512Mi 可能触发 OOM

如果你希望固定 Chart 或镜像版本,可显式加上 --version <chartVersion>--set image.tag=<controllerTag>。省略这些参数时,Helm 会使用仓库当前最新稳定版本以及 Chart 默认镜像。

调度机制补充:controllerManager Pod 由 Kubernetes Scheduler 负责调度,先筛掉不满足资源请求(至少 200m CPU 与 256Mi 内存)的节点,再根据集群当前负载打分,最终选择最合适的节点。如果没有额外的 nodeSelector、污点容忍或亲和性设置,它可能落在任意一个满足条件的节点上,但结果是依据算法和节点状态计算出来的,而非完全随机。

如果集群启用了 kube-routerCilium 等网络策略,请确保 cnpg-system 命名空间内的控制器可以访问 Kubernetes API 与目标命名空间。

安装完成后,验证 Operator 是否就绪:

kubectl get pods -n cnpg-system
kubectl get crds | grep postgresql.cnpg.io

你应该能看到 cluster.postgresql.cnpg.io 等 CRD 已注册,Operator Pod 处于 Running 状态。

355.webp

步骤二:准备命名空间与访问凭证

安装 Sealed Secrets

本次教程里,我们需要使用 Sealed Secrets 管理敏感信息。使用 Sealed Secrets 前,需要在集群部署控制器并在本地安装 kubeseal CLI:

常见问题

  • Q:为什么要引入 Sealed Secrets?
    A:Sealed Secrets 可以把 Kubernetes Secret 加密成 Git 可安全保存的清单。即使存入版本库也不会泄露明文,集群侧的控制器会按权限解封生成真实 Secret,让敏感配置可以使用标准 GitOps 流程同步。
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm repo update

kubectl create namespace sealed-secrets

helm upgrade --install sealed-secrets sealed-secrets/sealed-secrets \
--namespace sealed-secrets \
--set fullnameOverride=sealed-secrets-controller

上述命令会在 sealed-secrets 命名空间运行控制器,名称固定为 sealed-secrets-controller,便于后续示例引用。若集群已有现成部署,可跳过此步骤。

下一步,在本地安装 kubeseal 并执行 kubeseal --version 验证版本,请参考官方安装教程。

363.webp

注意:kubeseal 默认会去 kube-system 命名空间寻找名为 sealed-secrets-controller 的服务。如果像示例一样放在 sealed-secrets 命名空间,且没有指定参数,就会看到 error: cannot get sealed secret service: services "sealed-secrets-controller" not found。因此后续命令务必显式传入 --controller-namespace sealed-secrets --controller-name sealed-secrets-controller,或根据实际安装位置调整。

常见问题

  • Q:为什么运行 kubeseal 会提示 error: cannot get sealed secret service: services "sealed-secrets-controller" not found
    A:kubeseal 会默认去 kube-system 命名空间寻找同名服务。示例中我们把控制器部署在 sealed-secrets 命名空间,因此需要加上 --controller-namespace sealed-secrets --controller-name sealed-secrets-controller,否则就会找不到。
  • Q:既然如此,为什么不直接把服务部署在 kubeseal 默认能找到的 kube-system 命名空间?
    A:主要是为了隔离和易维护。kube-system 一般由集群底层组件占用,保持该命名空间纯净便于排障。同时独立的 sealed-secrets 命名空间能集中管理相关资源,在多环境或多租户时也方便做权限控制。工具默认指向 kube-system 是为了兼容常见安装方式,我们只需按实际部署传参即可。

生成业务 Secret

为数据库层准备单独的命名空间,并使用 Sealed Secrets 管理敏感信息,避免杂项资源混在同一空间:

kubectl create namespace data-infra

生成数据库应用账户的加密 Secret:

Step 1:生成原始 Secret 清单(不会写入集群)

kubectl create secret generic cnpg-app-user \
--namespace data-infra \
--from-literal=username=app_user \
--from-literal=password="$(openssl rand -base64 20)" \
--dry-run=client -o json > cnpg-app-user.secret.json

该命令将用户名和随机密码封装成 Kubernetes Secret 的 JSON 清单并写入 cnpg-app-user.secret.json,同时保持对集群无副作用。生成的文件内容类似:

{
"kind": "Secret",
"apiVersion": "v1",
"metadata": {
"name": "cnpg-app-user",
"namespace": "data-infra"
},
"data": {
"password": "random-password",
"username": "random-username"
}
}

Step 2:使用 kubeseal 加密成 SealedSecret

kubeseal \
--controller-namespace sealed-secrets \
--controller-name sealed-secrets-controller \
--format yaml < cnpg-app-user.secret.json > cnpg-app-user.sealed-secret.yaml

这里手动读取前一步的 JSON 并交给 kubeseal。命名空间和控制器参数确保 CLI 能连接上我们部署的 Sealed Secrets 控制器,--format yaml 则输出可提交到 Git 的 SealedSecret 文件,此处生成的文件内容类似于:

---
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: cnpg-app-user
namespace: data-infra
spec:
encryptedData:
password: random-password(locked)
username: random-username(locked)
template:
metadata:
name: cnpg-app-user
namespace: data-infra

Step 3:将 SealedSecret 应用到集群并删除原始 Secret

kubectl apply -f cnpg-app-user.sealed-secret.yaml
rm cnpg-app-user.secret.json

控制器接收后会自动解封并生成同名的常规 Secret(cnpg-app-user),供后续数据库资源引用。妥善保管好cnpg-app-user.sealed-secret.yaml,以便在后续的CICD流程中使用。

准备 S3 兼容对象存储凭证

  1. 登录对象存储服务控制台(如 AWS S3、Wasabi、Backblaze B2 或自建 MinIO 集群),新建一个专用的 Bucket,示例命名 cnpg-backup,Region 选择距离 Kubernetes 集群最近的区域。
  2. 在 Bucket 的访问策略中关闭公共读写,只允许凭证访问;如供应商支持生命周期或版本管理,可根据合规需求提前配置。
  3. 进入凭证或密钥管理页面,为本教程创建一对 Access Key,至少授予目标 Bucket 的 Object ReadObject Write 权限,必要时可限制在特定前缀范围。
  4. 记录控制台返回的 Access Key IDSecret Access KeyRegion(若适用)以及 S3 Endpoint,例如 https://s3.ap-northeast-1.amazonaws.com 或供应商提供的自定义域名。部分厂商会注明是否要求 Path Style 访问,请同步记下。
  5. 将这些信息保存到密码库,并准备好后续写入 Kubernetes Secret 的明文值:aws_access_key_idaws_secret_access_keyregion(若未固定 Region 可填供应商推荐值),同时确认 CNPG 中将使用的 destinationPath(形如 s3://my-cnpg-backups/production)。

完成控制台配置后,即可继续使用 Sealed Secrets 管理这组凭证喵。

使用 SealedSecret 配置对象存储凭证

计划启用对象存储备份时,同样使用 Sealed Secrets 生成访问凭证(以 S3 兼容服务为例):

kubectl create secret generic cnpg-backup-s3 \
--namespace data-infra \
--from-literal=aws_access_key_id=AKIA... \
--from-literal=aws_secret_access_key=xxxxxxxxxxxx \
--from-literal=region=ap-northeast-1 \
--dry-run=client -o json \
| kubeseal \
--controller-namespace sealed-secrets \
--controller-name sealed-secrets-controller \
--format yaml > cnpg-backup-s3.sealed-secret.yaml

kubectl apply -f cnpg-backup-s3.sealed-secret.yaml

步骤三:声明 PostgreSQL 集群

CloudNativePG 使用 Cluster 自定义资源来定义数据库栈。下面是一个生产友好的示例(存储类使用上一教程部署好的 Longhorn):

apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: cnpg-ha
namespace: data-infra
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.4
primaryUpdateStrategy: unsupervised
storage:
size: 40Gi
storageClass: longhorn
postgresql:
parameters:
max_connections: "200"
shared_buffers: "512MB"
wal_level: logical
pg_hba:
- hostssl all all 0.0.0.0/0 md5
authentication:
superuser:
secret:
name: cnpg-app-user
replication:
secret:
name: cnpg-app-user
bootstrap:
initdb:
database: appdb
owner: app_user
secret:
name: cnpg-app-user
backup:
barmanObjectStore:
destinationPath: s3://my-cnpg-backups/production
endpointURL: https://s3.ap-northeast-1.amazonaws.com
s3Credentials:
accessKeyId:
name: cnpg-backup-s3
key: aws_access_key_id
secretAccessKey:
name: cnpg-backup-s3
key: aws_secret_access_key
region:
name: cnpg-backup-s3
key: region
retentionPolicy: "14d"
monitoring:
enablePodMonitor: true

保存为 cnpg-ha.yaml 并应用:

kubectl apply -f cnpg-ha.yaml

CNPG 会自动创建 StatefulSet、PVC、服务等资源。等待 Pod 启动完成:

kubectl get pods -n data-infra -l cnpg.io/cluster=cnpg-ha -w

当 Primary 与 Replica 都处于 Running 状态时,数据库集群即部署完成。

步骤四:验证高可用与服务暴露

检查服务与连接信息

kubectl get svc -n data-infra -l cnpg.io/cluster=cnpg-ha

kubectl port-forward svc/cnpg-ha-rw 6432:5432 -n data-infra
psql "postgresql://app_user:$(kubectl get secret cnpg-app-user -n data-infra -o jsonpath='{.data.password}' | base64 -d)@127.0.0.1:6432/appdb"

CNPG 默认会创建 -rw(读写)与 -ro(只读)Service,方便应用分别接入主库与备库。

模拟故障切换

强制删除 Primary Pod 验证自动恢复能力:

kubectl delete pod -n data-infra \
$(kubectl get pods -n data-infra -l cnpg.io/cluster=cnpg-ha,role=primary -o jsonpath='{.items[0].metadata.name}')

几秒钟内,CNPG 会自动将一个 Replica 提升为 Primary。通过下列命令确认角色变化:

kubectl get pods -n data-infra -L role
kubectl logs -n data-infra -l cnpg.io/cluster=cnpg-ha,role=primary --tail=20

步骤五:备份与灾备策略

  • 持续备份(PITR)barmanObjectStore 配置将 WAL 持续写入对象存储,可通过 kubectl cnpg backup 命令触发全量备份。
  • 定期演练恢复:在临时命名空间中创建 Backup + Cluster CR,验证从对象存储恢复到指定时间点。
  • 监控与告警:开启 monitoring.enablePodMonitor 后,可在 Prometheus 中抓取 cnpg_pg_replication_lagcnpg_pg_is_in_recovery 等关键指标。
  • 资源配额:为 data-infra 命名空间配置 LimitRangeResourceQuota,防止数据库实例挤占节点资源。

常见运维操作

  • 在线扩容:调整 spec.instances 数量即可,CNPG 会自动创建新的 Replica 并同步数据。
  • 参数调整:更新 spec.postgresql.parameters,Operator 会触发滚动重启完成配置下发。
  • 版本升级:修改 spec.imageName 指定新版本镜像,CNPG 会先创建新 Replica,执行 switchover 后再回收旧主库。
  • 逻辑复制:通过 pg_hbawal_level 配置,可以将特定库/表同步到外部数据仓库。

总结与下一步

至此,我们已经在高可用 k3s 集群上部署了一套具备自动故障切换、持续备份、声明式管理能力的 PostgreSQL 集群。下一步可以考虑:

  1. 引入 Grafana LokiELK 收集数据库日志,完善观测能力。
  2. 使用 CloudNativePG PoolerPgBouncer 优化连接池管理。
  3. 结合 Argo CD/Fleet 等 GitOps 工具,将数据库 CR 纳入持续交付流程,实现配置变更审核与回滚。